feat: add agent-managed forked side threads#4246
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| thread_list: (input: { readonly includeArchived?: boolean | undefined }) => | ||
| Effect.gen(function* () { | ||
| const current = yield* currentThread(); | ||
| const snapshot = yield* query.getShellSnapshot(); |
There was a problem hiding this comment.
🟡 Medium threads/handlers.ts:103
thread_list ignores includeArchived: true — archived threads are always excluded from the result, even when the caller explicitly requests them. The handler only calls query.getShellSnapshot(), which does not return archived threads, so the subsequent input.includeArchived === true || thread.archivedAt === null filter can never surface archived entries because they were never in the snapshot to begin with. To honor includeArchived, the handler must also query (and merge) getArchivedShellSnapshot() when archived entries are requested.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/mcp/toolkits/threads/handlers.ts around line 103:
`thread_list` ignores `includeArchived: true` — archived threads are always excluded from the result, even when the caller explicitly requests them. The handler only calls `query.getShellSnapshot()`, which does not return archived threads, so the subsequent `input.includeArchived === true || thread.archivedAt === null` filter can never surface archived entries because they were never in the snapshot to begin with. To honor `includeArchived`, the handler must also query (and merge) `getArchivedShellSnapshot()` when archived entries are requested.
| }); | ||
| } | ||
|
|
||
| const cutoffIndex = |
There was a problem hiding this comment.
🟡 Medium orchestration/decider.ts:308
When the source thread has no completed assistant turn (e.g., forking during the first running turn), sourceTurnId is null and the fork inherits all non-streaming messages from the running turn — including the user message whose turnId is null — instead of producing empty history. With no completed turn to fork from, the cutoff should yield no inherited messages, not every non-streaming message in the thread. Consider treating sourceTurnId === null as an empty cutoff rather than slicing through the end of the message list.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/decider.ts around line 308:
When the source thread has no completed assistant turn (e.g., forking during the first running turn), `sourceTurnId` is `null` and the fork inherits all non-streaming messages from the running turn — including the user message whose `turnId` is `null` — instead of producing empty history. With no completed turn to fork from, the cutoff should yield no inherited messages, not every non-streaming message in the thread. Consider treating `sourceTurnId === null` as an empty cutoff rather than slicing through the end of the message list.
| )(function* (event, attachmentSideEffects) { | ||
| switch (event.type) { | ||
| case "thread.forked": | ||
| yield* Effect.forEach( |
There was a problem hiding this comment.
🟠 High Layers/ProjectionPipeline.ts:844
When a forked thread is later reverted, the entire inherited conversation is deleted from the projected fork instead of retaining its baseline history. The thread.forked projector preserves each inherited message's original turnId (line ~850) but never creates corresponding ProjectionTurn rows for those source turn IDs. On thread.reverted, retainProjectionMessagesAfterRevert keeps a message only when its turnId exists among the fork's retained projection turns — and inherited source turn IDs never match, so all inherited messages are dropped. Consider materializing ProjectionTurn rows for inherited turn IDs when handling thread.forked, or adjusting the revert retention logic so inherited baseline messages are preserved regardless of turn membership.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProjectionPipeline.ts around line 844:
When a forked thread is later reverted, the entire inherited conversation is deleted from the projected fork instead of retaining its baseline history. The `thread.forked` projector preserves each inherited message's original `turnId` (line ~850) but never creates corresponding `ProjectionTurn` rows for those source turn IDs. On `thread.reverted`, `retainProjectionMessagesAfterRevert` keeps a message only when its `turnId` exists among the fork's retained projection turns — and inherited source turn IDs never match, so all inherited messages are dropped. Consider materializing `ProjectionTurn` rows for inherited turn IDs when handling `thread.forked`, or adjusting the revert retention logic so inherited baseline messages are preserved regardless of turn membership.
| ...(input.forkFrom !== undefined && isCodexResumeCursorSchema(input.forkFrom.resumeCursor) | ||
| ? { | ||
| forkFrom: { | ||
| providerThreadId: input.forkFrom.resumeCursor.threadId, | ||
| ...(input.forkFrom.sourceTurnId !== undefined | ||
| ? { sourceTurnId: input.forkFrom.sourceTurnId } | ||
| : {}), | ||
| }, | ||
| } | ||
| : {}), |
There was a problem hiding this comment.
🟠 High Layers/CodexAdapter.ts:1409
startSession silently drops input.forkFrom when input.forkFrom.resumeCursor fails isCodexResumeCursorSchema, starting a fresh Codex thread instead of the requested fork. The adapter still advertises sessionFork: "native", so a caller passing a non-Codex (e.g. legacy or malformed) persisted cursor succeeds with a side thread that has no link to the source conversation, rather than failing the fork request. Consider returning a ProviderAdapterValidationError in this branch instead of silently omitting forkFrom.
- ...(input.forkFrom !== undefined && isCodexResumeCursorSchema(input.forkFrom.resumeCursor)
+ ...(input.forkFrom !== undefined
+ ? isCodexResumeCursorSchema(input.forkFrom.resumeCursor)
+ ? {
+ forkFrom: {
+ providerThreadId: input.forkFrom.resumeCursor.threadId,
+ ...(input.forkFrom.sourceTurnId !== undefined
+ ? { sourceTurnId: input.forkFrom.sourceTurnId }
+ : {}),
+ },
+ }
+ : yield* new ProviderAdapterValidationError({
+ provider: PROVIDER,
+ operation: "startSession",
+ issue: "forkFrom.resumeCursor is not a valid Codex resume cursor.",
+ })
+ : {}),🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around lines 1409-1418:
`startSession` silently drops `input.forkFrom` when `input.forkFrom.resumeCursor` fails `isCodexResumeCursorSchema`, starting a fresh Codex thread instead of the requested fork. The adapter still advertises `sessionFork: "native"`, so a caller passing a non-Codex (e.g. legacy or malformed) persisted cursor succeeds with a side thread that has no link to the source conversation, rather than failing the fork request. Consider returning a `ProviderAdapterValidationError` in this branch instead of silently omitting `forkFrom`.
|
Superseded by #4248, which rebuilds this as the standalone selected-message fork feature and excludes all agent thread controls. |
What Changed
thread.forkorchestration command that copies conversation history through a completed turn and persists the fork lineage.thread_list,thread_create,thread_fork,thread_send, andthread_archiveMCP tools so an agent can coordinate sibling work without leaving T3 Code.thread/forksupport so a side thread retains native provider context.thread_forkchooses its default fork point. This lets an agent call the tool during its own response while inheriting the last completed turn.Why
Agents can currently work only in the active conversation. Delegating an investigation or exploring an alternative requires losing context, manually creating another thread, or interrupting the main task.
This adds one project-native coordination path: an agent can fork the current conversation, open the result beside the main chat, and continue both threads independently while T3 Code retains their lineage.
Scope
This is one end-to-end feature, but it crosses the orchestration schema, projection storage, provider lifecycle, MCP surface, client commands, and side-panel UI. The layers are submitted together because a partial implementation would either lose lineage/context or expose a non-functional tool.
Verification
vp test runon the six affected MCP, orchestration, migration, Codex runtime, and right-panel test files: 56 tests passed.thread_forkduring its own running turn, open the side chat, and inherit the completed source history.UI Evidence
Before
After
Interaction
Short agent-managed fork recording
Checklist
Note
Add agent-managed forked side threads with MCP toolkit and native Codex fork support
thread.forkcommand andthread.forkedevent in the contracts layer, allowing threads to be forked from a source thread at a specific turn, with inherited messages carried into the new thread.thread.forkedwith lineage and inherited messages; the projector and projection pipeline update the read model and DB accordingly.forked_from_thread_idandforked_from_turn_idcolumns plus an index toprojection_threads.CodexAdapterandCodexSessionRuntimevia athread/forkprovider request, withProviderService.startSessionvalidating adapter capabilities and source thread binding before passing fork metadata.thread_list,thread_create,thread_fork,thread_send,thread_archive) registered on the MCP server, with developer instructions updated to cover thread coordination tools.ChatHeaderthat creates a forked thread and opens it in the right panel as an embeddedChatView; newly detected forked child threads auto-open in the right panel within 2 seconds of mount.RIGHT_PANEL_STORAGE_VERSIONto 8 will reset persisted right-panel state for existing users.📊 Macroscope summarized 41bbfc5. 29 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.